package org.ddevil.data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.ddevil.data.util.DataSetUtils;

/**
 * A data structure with some intelligence for tracking changes. Data
 * objects are used to store old and new values. It is assumed that any two
 * MSWPAttribute objects with the same set of DB Id's and all the same values
 * for those DB Id's are the same record. This assumption is used to make
 * decisions on what changes to actually log. So if a record is added but it is
 * determined that the same record was previously removed, both changes negate
 * each other and neither is logged. If the previous assumption is not true then
 * you have to put the change tracker in 'dumb' mode and all changes will be
 * logged, including duplicates. If not the results are unpredictable.
 *
 *
 * XXX THIS CLASS IS STILL IN DEVELOPMENT XXX
 *
 * @author Rich
 */
public class ChangeTracker implements Serializable {

	private static final long serialVersionUID = -3618629177527256917L;

	private ArrayList<Change> changes;

	public enum ChangeTypes /*implements Serializable*/ {
		ADD, REMOVE, MODIFY
	}


	protected boolean dumbMode;

    /**
     * Create a new Change Tracker.
     */
	public ChangeTracker() {
		changes = new ArrayList<Change>();
		dumbMode = false;
	}

	/**
	 * Put this ChangeTracker in 'dumb' mode. In 'dumb' mode all requested
	 * changes are logged no matter if they negate a previous change or nothing
	 * has actually changed. By default we are in 'smart' mode.
	 *
	 * @param boo
	 *            TRUE - put in dumb mode FALSE - put in smart mode
	 */
	public void setDumbMode(boolean boo) {
		dumbMode = boo;
	}

	public void clear() {
		changes = new ArrayList<Change>();
	}

	public List<Change> getChanges() {
		return changes;
	}

	private List<Change> getAllOfType(ChangeTypes type) {
		ArrayList<Change> subset = new ArrayList<Change>();
		for (int i = 0; i < changes.size(); i++) {
			Change currentChange = changes.get(i);
			if (currentChange.getType() == type) {
				subset.add(currentChange);
			}
		}
		return subset;
	}

	public List<Change> getAddRecords() {
		return getAllOfType(ChangeTypes.ADD);
	}

	public List<Change> getModifiedRecords() {
		return getAllOfType(ChangeTypes.MODIFY);
	}

	public List<Change> getRemovedRecords() {
		return getAllOfType(ChangeTypes.REMOVE);
	}

	public boolean hasChanges() {
		return changes.size() > 0;
	}

	public void itemAdded(Data item) {
		if (dumbMode) {
			changes.add(new Change(ChangeTypes.ADD, null, item));
		} else {

			if (validateAdd(item))
				changes.add(new Change(ChangeTypes.ADD, null, item));
		}
	}

	private boolean validateAdd(Data add) {
		for (int i = 0; i < changes.size(); i++) {
			Change c = changes.get(i);
			if (c.getType() == ChangeTypes.REMOVE) {
				// check the remove to see if it is the item being added
				if (compareAttribs(add, c.getOldValue())) {
					// the item being added has allready been removed,
					// this negates the change.
					changes.remove(c);
					return false;
				}
			}
		}
		return true;
	}

	public void itemRemoved(Data item) {
		if (dumbMode) {
			changes.add(new Change(ChangeTypes.REMOVE, item, null));
		} else {

			if (validateRemove(item))
				changes.add(new Change(ChangeTypes.REMOVE, item, null));
		}
	}

	private boolean validateRemove(Data remove) {
		// first check to see if we have a modification on this record
		for (int i = 0; i < changes.size(); i++) {
			Change c = changes.get(i);
			if (c.getType() == ChangeTypes.MODIFY) {
				if (compareAttribs(c.getNewValue(), remove)) {
					// the record being removed has also been modified.
					// remove the mod change
					changes.remove(c);
				}
			}
		}

		for (int i = 0; i < changes.size(); i++) {
			Change c = changes.get(i);
			if (c.getType() == ChangeTypes.ADD) {
				// check the add to see if it is the item being removed
				if (compareAttribs(remove, c.getNewValue())) {
					// the item being removed has allready been added,
					// this negates the change.
					changes.remove(c);
					return false;
				}
			}
		}
		return true;
	}

	public void itemModified(Data oldVal, Data newVal) {
		if (dumbMode) {
			changes.add(new Change(ChangeTypes.MODIFY, oldVal, newVal));
		} else {

			// if the two vals are equal then its not really a modification is
			// it
			if (compareAttribs(oldVal, newVal)) {
				return;
			}

			boolean found = false;
			for (int i = 0; i < changes.size(); i++) {

				Change c = changes.get(i);
				if (c.getType() == ChangeTypes.MODIFY) {

					if (compareAttribs(oldVal, c.getNewValue())) {
						// this record has allready been modified

						if (compareAttribs(c.getOldValue(), newVal)) {
							// we are changing it back to its original state.
							// remove the change record
							changes.remove(c);

						} else {
							// just update the Change record with the newVal
							c.setNewValue(newVal);
						}
						found = true;
						break;
					}
				}
			}

			if (!found) {
				// this is a new mod, add it to the list
				changes.add(new Change(ChangeTypes.MODIFY, oldVal, newVal));
			}
		}
	}

	/*
	 * Compare two attribute objects.
	 */
	private static boolean compareAttribs(Data first,
			Data second) {
		return DataSetUtils.attribsEqual(first, second);

	}

	public static class Change implements Serializable {

		private ChangeTypes type;

		private Data oldVal;

		private Data newVal;

		public Change(ChangeTypes type, Data oldValue,
				Data newValue) {

			setType(type);
			setOldValue(oldValue);
			setNewValue(newValue);
		}

		/**
		 * @return Returns the newVal.
		 */
		public Data getNewValue() {
			return newVal;
		}

		/**
		 * @param newVal
		 *            The newVal to set.
		 */
		public void setNewValue(Data newVal) {
			this.newVal = newVal;
		}

		/**
		 * @return Returns the oldVal.
		 */
		public Data getOldValue() {
			return oldVal;
		}

		/**
		 * @param oldVal
		 *            The oldVal to set.
		 */
		public void setOldValue(Data oldVal) {
			this.oldVal = oldVal;
		}

		/**
		 * @param type
		 *            The type to set.
		 */
		public void setType(ChangeTypes type) {
			this.type = type;
		}

		public ChangeTypes getType() {
			return this.type;
		}

		@Override
		public String toString() {
			StringBuilder sb = new StringBuilder();
			sb.append(type);
			sb.append("\nOLD VALUE:\n" + oldVal);
			sb.append("\nNEW VALUE:\n" + newVal);
			return sb.toString();
		}

	}

}
